El objetivo de este notebook es crear un diccionario Español - Inglés
import numpy as np
import gensim
import subprocess
import os
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
import wordcloud
from IPython.core.pylabtools import figsize
figsize(50, 50)
from sklearn.manifold import TSNE
import json
from collections import Counter
from itertools import chain
import string
!pip install unidecode
import unidecode # para quitar los acentos en caso de que haya en español
import re
from numpy import array, argmax, random, take
import pandas as pd
%tensorflow_version 1.x
import tensorflow
print(tensorflow.__version__)
from keras.utils import get_file
from keras.models import Sequential
from keras.layers import Dense, LSTM, Embedding, Bidirectional, RepeatVector, TimeDistributed, GRU, CuDNNLSTM, Bidirectional, Dropout
from keras.preprocessing.text import Tokenizer
from keras.callbacks import ModelCheckpoint
from keras.preprocessing.sequence import pad_sequences
from keras.models import load_model
from keras import optimizers
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option('display.max_colwidth', 200)
Los datos es un archivo de texto de parejas de frases en Inglés y Español. Primero leemos los datos del archivo
# function to read raw text file
def read_text(filename):
# open the file
file = open(filename, mode='rt', encoding='utf-8')
# read all text
text = file.read()
file.close()
return text
Separamos el texto en pares usando el salto de línea \n y separamos los pares en frases en Inglés y Español con el tabulador \t
# split a text into sentences
def to_lines(text):
sents = text.strip().split('\n')
sents = [i.split('\t') for i in sents]
return sents
Cargamos los datos desde Drive
from google.colab import drive
drive.mount('/content/drive')
# Navigate to code directory
%cd /content/drive/My Drive/DeliverableTEXT
# List project directory contents
!ls
data = read_text("./spa-eng/spa.txt")
clean_data = [i[0:2] for i in to_lines(data)] # Leemos solo las dos primeras columnas
esp_eng = array(clean_data)
len(esp_eng)
Los datos contienen 122936 pares. Utilizaremos todos ellos para el entrenamiento del modelo, aunque si quisiesemos reducir el tiempo de entrenamiento podríamos utilizar menos datos
#esp_eng = esp_eng[:50000,:] # Si quisiesemos un dataset reducido
Echamos un vistazo a los datos y después decidimos que procesos seguir
esp_eng
Quitamos los signos de puntuación y lo pasamos a minúscula.
# Remove punctuation
esp_eng[:,0] = [s.translate(str.maketrans('', '', string.punctuation)) for s in esp_eng[:,0]] # En las frases en ingles
esp_eng[:,1] = [s.translate(str.maketrans('', '', string.punctuation)) for s in esp_eng[:,1]] # En las frases en español
esp_eng
# convert to lowercase and remove accents
for i in range(len(esp_eng)):
esp_eng[i,0] = unidecode.unidecode(esp_eng[i,0].lower())
esp_eng[i,1] = unidecode.unidecode(esp_eng[i,1].lower())
esp_eng
words_in_str = [i.replace("\n","").replace("!","").replace("?","").split() for i in esp_eng[:,0:2].flatten()]
all_words = [ii for i in words_in_str for ii in i]
tokens = [i for i in all_words if (len(i)>1) and i not in stopwords.words('spanish')]
tokens2 = [i for i in tokens if (len(i)>1) and i not in stopwords.words('english')]
nltk.FreqDist.plot(nltk.FreqDist(tokens2), 25)
wc2 = wordcloud.WordCloud(
width=1000,
height=1000,
max_words=100,
collocations=False
).generate(text=(' '.join(tokens2)))
get_ipython().run_line_magic('matplotlib', 'inline')
plt.figure(figsize=[10,10])
plt.imshow(wc2)
plt.axis("off");
Como se puede ver, las palabras más comunes en las traducciones suelen ser o nombres propios o verbos comunes como querer pensar, poder, venir, saber...
Podemos intentar ver las palabras más raras de las traducciones mediante técnicas de words embeddings
MODEL = 'GoogleNews-vectors-negative300.bin'
path= get_file(MODEL + '.gz','https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz')
#path = get_file(MODEL + '.gz', 'https://deeplearning4jblob.blob.core.windows.net/resources/wordvectors/%s.gz' % MODEL)
if not os.path.isdir('generated'):
os.mkdir('generated')
unzipped = os.path.join('generated', MODEL)
if not os.path.isfile(unzipped):
with open(unzipped, 'wb') as fout:
zcat = subprocess.Popen(['zcat'],
stdin=open(path),
stdout=fout
)
zcat.wait()
model = gensim.models.KeyedVectors.load_word2vec_format(unzipped, binary=True)
item_vectors = [(item, model[item])
for item in list(set(tokens2)) # Hacemos un listado de las palabras únicas
if item in model] # lo que hacemos es encontrar las representaciones de las palabras en nuestro modelo
len(item_vectors)
Si usamos t-SNE veríamos las palabras mas raras de las traducciones como aquellas que se encuentren más alejadas del resto
vectors = np.asarray([x[1] for x in item_vectors])
lengths = np.linalg.norm(vectors, axis=1)
norm_vectors = (vectors.T / lengths).T
tsne = TSNE(n_components=2, perplexity=10, verbose=2).fit_transform(norm_vectors)
x=tsne[:,0]
y=tsne[:,1]
fig, ax = plt.subplots()
ax.scatter(x, y)
for item, x1, y1 in zip(item_vectors, x, y):
ax.annotate(item[0], (x1, y1), size=20)
plt.figure(figsize=[50,50])
plt.show()
Podemos ver que algunas de las palabras más complejas pueden ser vivas, escala, comercial y aso. Todas ellas son palabras complejas o bien porque se tratan de palabras que se utilizan en contextos específicos o bien porque son formas verbales avanzadas (imperativos).
Para entrenar el modelo de Seq2Seq convertiremos la salida y la entrada en secuencias de enteros. Antes de eso visualizaremos la longitud de las frases y capturaremos estas longitudes en dos arrays distintas.
# populate the lists with sentence lengths
eng_l = [len(i.split()) for i in esp_eng[:,0]]
esp_l = [len(i.split()) for i in esp_eng[:,1]]
length_df = pd.DataFrame({'eng':eng_l, 'esp':esp_l})
print(length_df)
print(max(length_df.eng))
print(max(length_df.esp))
max_length = max(max(length_df.eng), max(length_df.esp)) # cogemos la maxima longitud automaticamente
print(max_length)
length_df.hist(bins = 30)
plt.show()
La longitud maxima de las frases en español es 12 y en las de inglés es 8.
Vectorizamos los datos usando la clase Tokenizer() de Keras. Esto transformará nuestras frases en secuencias de enterior. Después usaremos padding para que todas tengan la misma longitud.
# function to build a tokenizer
def tokenization(lines):
tokenizer = Tokenizer(char_level = False) # editamos la funcion de tokenizacion
tokenizer.fit_on_texts(lines)
return tokenizer
# prepare english tokenizer
eng_tokenizer = tokenization(esp_eng[:, 0])
eng_vocab_size = len(eng_tokenizer.word_index) + 1
eng_length = max_length # max length (deberia ser 8 pero lo fijamos a la maxima de los dos idiomas para coger todo)
print('English Vocabulary Size: %d' % eng_vocab_size)
# prepare spanish tokenizer
esp_tokenizer = tokenization(esp_eng[:, 1])
esp_vocab_size = len(esp_tokenizer.word_index) + 1
esp_length = eng_length # la longitud de la entrada es la misma que la de la salida
print('Spanish Vocabulary Size: %d' % esp_vocab_size)
print(str(esp_tokenizer.word_counts)[0:145])
La siguiente función nos prepara las secuencias. También aplicará padding para ajustar todas las frases a la misma longitud.
# encode and pad sequences
def encode_sequences(tokenizer, length, lines):
# integer encode sequences
seq = tokenizer.texts_to_sequences(lines)
# pad sequences with 0 values
seq = pad_sequences(seq, maxlen=length, padding='post')
print(seq)
print(len(seq))
return seq
Dividimos los datos en entrenamiento y test
from sklearn.model_selection import train_test_split
train, test = train_test_split(esp_eng, test_size=0.2, random_state = 12)
Ahora hacemos encoding a las frases. Hacemos encode a las frases en español como las secuencias de entrada y las de inglés como las secuencias de salida, tanto para entrenamiento como para test.
# prepare training data
trainX = encode_sequences(esp_tokenizer, esp_length, train[:, 1])
trainY = encode_sequences(eng_tokenizer, eng_length, train[:, 0])
print(trainX.shape)
print(trainY.shape)
# prepare validation data
testX = encode_sequences(esp_tokenizer, esp_length, test[:, 1])
testY = encode_sequences(eng_tokenizer, eng_length, test[:, 0])
print(testX.shape)
print(testY.shape)
Ahora se define la arquitectura del modelo Seq2Seq. Usamos una capa Embedding y una capa de LSTM como encoder y otra LSTM y una Dense como decoder.
# build NMT model
def build_model(in_vocab, out_vocab, in_timesteps, out_timesteps, units):
model = Sequential()
model.add(Embedding(in_vocab, units, input_length=in_timesteps, mask_zero=True))
model.add(LSTM(units))
model.add(RepeatVector(out_timesteps))
model.add(LSTM(units, return_sequences=True))
model.add(Dense(out_vocab, activation='softmax'))
return model
Details about the RepeatVector : https://campus.datacamp.com/courses/machine-translation-in-python/implementing-an-encoder-decoder-model-with-keras?ex=6
Usamos un optimizador RMSprop para el modelo ya que usualmente es una buena elección para redes neuronales recurrentes.
print(esp_vocab_size)
print(eng_vocab_size)
print(esp_length)
print(eng_length)
model1 = build_model(esp_vocab_size, eng_vocab_size, esp_length, eng_length, 512)
rms = optimizers.RMSprop(lr=0.001)
model1.compile(optimizer=rms, loss='sparse_categorical_crossentropy',
metrics = ['accuracy'])
Usamos 'sparse_categorical_crossentropy' como función de perdida porque nos permite usar la secuencia de salida como tal en lugar del formato one hot encoded. Hacer One Hot encoding en la secuencia de salida con un vocabulario tan grande seguramente consuma toda la memoria de nuestro sistema.
Entrenaremos el modelo con 30 epoch y un batch size de 512, aunque estos hiperparametros se pueden modificar. Usaremos ModelCheckpoint() para guardar el mejor modelo con mejor perdida en validación.
filename = 'model.h1_loss2'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
history1 = model1.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1),
epochs=10, batch_size=128,
validation_split = 0.2,
callbacks=[checkpoint], verbose=1)
Tenemos una precisión en validación del 92,7%
Comparamos la perdida en entrenamiento y validación.
plt.plot(history1.history['accuracy'])
plt.plot(history1.history['val_accuracy'])
plt.legend(['train','validation'])
plt.show()
plt.plot(history1.history['loss'])
plt.plot(history1.history['val_loss'])
plt.legend(['train','validation'])
plt.show()
Cargamos el modelo para hacer predicciones
model1 = load_model('model.h1_loss2')
preds = model1.predict_classes(testX[:1000,:].reshape((1000,testX.shape[1])))
#preds = model1.predict_classes(testX.reshape((testX.shape[0],testX.shape[1]))) # Muy costoso computacionalmente
def get_word(n, tokenizer):
for word, index in tokenizer.word_index.items():
if index == n:
return word
return None
# convert predictions into text (English)
preds_text = []
for i in preds:
temp = []
for j in range(len(i)):
t = get_word(i[j], eng_tokenizer)
if j > 0:
if (t == get_word(i[j-1], eng_tokenizer)) or (t == None):
temp.append('')
else:
temp.append(t)
else:
if(t == None):
temp.append('')
else:
temp.append(t)
preds_text.append(' '.join(temp))
pred_df1 = pd.DataFrame({'actual' : test[:1000,0], 'predicted' : preds_text})
pd.set_option('display.max_colwidth', 200)
pred_df1.head(15)
pred_df1.tail(15)
pred_df1.sample(15)
Usando un GRU en lugar de LTSM
# build NMT model
def build_simpler_model(in_vocab, out_vocab, in_timesteps, out_timesteps, units):
model = Sequential()
model.add(Embedding(in_vocab, units, input_length=in_timesteps, mask_zero=True))
model.add(GRU(units)) # GRU instead of LSTM
model.add(RepeatVector(out_timesteps))
model.add(GRU(units, return_sequences=True))
model.add(Dense(out_vocab, activation='softmax'))
return model
model2 = build_simpler_model(esp_vocab_size, eng_vocab_size, esp_length, eng_length, 512)
rms = optimizers.RMSprop(lr=0.001)
model2.compile(optimizer=rms, loss='sparse_categorical_crossentropy',
metrics = ['accuracy'])
Entrenaremos el modelo.
filename = 'model.h1_simpler'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
history2 = model2.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1),
epochs=10, batch_size=128,
validation_split = 0.2,
callbacks=[checkpoint], verbose=1)
Precisión en validación de 92,9%
Comparamos la perdida en entrenamiento y validación.
plt.plot(history2.history['accuracy'])
plt.plot(history2.history['val_accuracy'])
plt.legend(['train','validation'])
plt.show()
plt.plot(history2.history['loss'])
plt.plot(history2.history['val_loss'])
plt.legend(['train','validation'])
plt.show()
Parece que este modelo tiene una actuación parecida al que utiliza LSTM, sin embargo parece que sobreentrena más, hay mas distancia entre la curva de entranamiento y validación.
Cargamos el modelo para hacer predicciones
model2 = load_model('model.h1_simpler')
preds = model2.predict_classes(testX[:1000,:].reshape((1000,testX.shape[1])))
# convert predictions into text (English)
preds_text = []
for i in preds:
temp = []
for j in range(len(i)):
t = get_word(i[j], eng_tokenizer)
if j > 0:
if (t == get_word(i[j-1], eng_tokenizer)) or (t == None):
temp.append('')
else:
temp.append(t)
else:
if(t == None):
temp.append('')
else:
temp.append(t)
preds_text.append(' '.join(temp))
pred_df2 = pd.DataFrame({'actual' : test[:1000,0], 'predicted' : preds_text})
pd.set_option('display.max_colwidth', 200)
pred_df2.head(15)
pred_df2.tail(15)
pred_df2.sample(15)
Las traducciones son mas o menos parecidas
Para que no sobreentrene introducimos una capa de Dropout de 20%. También probaremos cambiando la función de activación por una sigmoid
# build NMT model
def build_sigmoid_model(in_vocab, out_vocab, in_timesteps, out_timesteps, units):
model = Sequential()
model.add(Embedding(in_vocab, units, input_length=in_timesteps, mask_zero=True))
model.add(LSTM(units))
model.add(RepeatVector(out_timesteps))
model.add(LSTM(units))
model.add(Bidirectional(GRU(units,return_sequences=True)))
model.add(Dropout(0.2))
model.add(Dense(out_vocab, activation='sigmoid')) # sigmoid en lugar de softmax
return model
model3 = build_sigmoid_model(esp_vocab_size, eng_vocab_size, esp_length, eng_length, 512)
rms = optimizers.RMSprop(lr=0.001)
model3.compile(optimizer=rms, loss='sparse_categorical_crossentropy',
metrics = ['accuracy'])
Entrenaremos el modelo.
filename = 'model.h1_sigmoid'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
history3 = model3.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1),
epochs=10, batch_size=128,
validation_split = 0.2,
callbacks=[checkpoint], verbose=1)
Precisión en validación de 92,7%
Comparamos la perdida en entrenamiento y validación.
plt.plot(history3.history['accuracy'])
plt.plot(history3.history['val_accuracy'])
plt.legend(['train','validation'])
plt.show()
plt.plot(history3.history['loss'])
plt.plot(history3.history['val_loss'])
plt.legend(['train','validation'])
plt.show()
Parece que este modelo tiene una actuación parecida primer modelo propuesto, parece que introducir la RNN bidireccional en las capas mas altas no ha influido mucho.
Cargamos el modelo para hacer predicciones
model3 = load_model('model.h1_sigmoid')
preds = model3.predict_classes(testX[:1000,:].reshape((1000,testX.shape[1])))
# convert predictions into text (English)
preds_text = []
for i in preds:
temp = []
for j in range(len(i)):
t = get_word(i[j], eng_tokenizer)
if j > 0:
if (t == get_word(i[j-1], eng_tokenizer)) or (t == None):
temp.append('')
else:
temp.append(t)
else:
if(t == None):
temp.append('')
else:
temp.append(t)
preds_text.append(' '.join(temp))
pred_df3 = pd.DataFrame({'actual' : test[:1000,0], 'predicted' : preds_text})
pd.set_option('display.max_colwidth', 200)
pred_df3.head(15)
pred_df3.tail(15)
pred_df3.sample(15)
Las traducciones son mas o menos parecidas
También probaremos a cambiar el optimizador por un Adam de lr=0.001
# build NMT model
def build_final_model(in_vocab, out_vocab, in_timesteps, out_timesteps, units):
model = Sequential()
model.add(Embedding(in_vocab, units,input_length=in_timesteps))
model.add(Bidirectional(CuDNNLSTM(units,return_sequences=False))) # Probamos con redes recurrentes bidireccionales
model.add(RepeatVector(out_timesteps))
model.add(Bidirectional(CuDNNLSTM(units,return_sequences=True)))
model.add(Dropout(0.1))
model.add(TimeDistributed(Dense(out_vocab,activation='softmax')))
return model
model4 = build_final_model(esp_vocab_size, eng_vocab_size, esp_length, eng_length, 512)
#rms = optimizers.RMSprop(lr=0.001)
#model.compile(optimizer=rms, loss='sparse_categorical_crossentropy')
model4.compile(loss = 'sparse_categorical_crossentropy',
optimizer = optimizers.Adam(learning_rate=0.001), # Probamos con un optimizador Adam
metrics = ['accuracy'])
Entrenaremos el modelo.
filename = 'model.h1_final'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
history4 = model4.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1),
epochs=10, batch_size=128,
validation_split = 0.2,
callbacks=[checkpoint], verbose=1)
Comparamos la perdida en entrenamiento y validación.
plt.plot(history4.history['accuracy'])
plt.plot(history4.history['val_accuracy'])
plt.legend(['train','validation'])
plt.show()
plt.plot(history4.history['loss'])
plt.plot(history4.history['val_loss'])
plt.legend(['train','validation'])
plt.show()
Este modelo es el que menos sobreentrena, los resultados entre entrenamiento y validación son tremendamente parecidos
Cargamos el modelo para hacer predicciones
model4 = load_model('model.h1_final')
preds = model4.predict_classes(testX[:1000,:].reshape((1000,testX.shape[1])))
# convert predictions into text (English)
preds_text = []
for i in preds:
temp = []
for j in range(len(i)):
t = get_word(i[j], eng_tokenizer)
if j > 0:
if (t == get_word(i[j-1], eng_tokenizer)) or (t == None):
temp.append('')
else:
temp.append(t)
else:
if(t == None):
temp.append('')
else:
temp.append(t)
preds_text.append(' '.join(temp))
pred_df4 = pd.DataFrame({'actual' : test[:1000,0], 'predicted' : preds_text})
pd.set_option('display.max_colwidth', 200)
pred_df4.head(15)
pred_df4.tail(15)
pred_df4.sample(15)
Parece que este modelo es el que peor funciona
print("Frases originales:")
print(pred_df1['actual'].head(5))
print("***************************")
print("Traducciones:")
print("***************************")
print("Modelo 1:")
print(pred_df1['predicted'].head(5))
print("Modelo 2:")
print(pred_df2['predicted'].head(5))
print("Modelo 3:")
print(pred_df3['predicted'].head(5))
print("Modelo 4:")
print(pred_df4['predicted'].head(5))
Como podemos ver, parece que el modelo que tiene mejor actuación es el primero de todos, las traducciones se ajustan mejor. Cabe destacar que las traducciones que mejor se ajustan son aquellas en las que la frecuencia de las palabras es más alta como veíamos en la WordCloud. Los ejemplos que tienen verbos como querer, tener, I am funcionan mejor que las palabras complejas como reliquias.
print("Frases originales:")
print(pred_df1['actual'].tail(5))
print("***************************")
print("Traducciones:")
print("***************************")
print("Modelo 1:")
print(pred_df1['predicted'].tail(5))
print("Modelo 2:")
print(pred_df2['predicted'].tail(5))
print("Modelo 3:")
print(pred_df3['predicted'].tail(5))
print("Modelo 4:")
print(pred_df4['predicted'].tail(5))
Aquí también parece que el primer modelo es el que mejor funciona